﻿namespace Silverlight
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Reflection.Emit;

    /// <summary>
    /// Provides generated DTO
    /// </summary>
    public class Connector
    {
        #region Data Fields
        /// <summary>
        /// Holds previously generated DTO types
        /// </summary>
        private Dictionary<string, Type> _cachedTypes;
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public Connector()
        {
            this._cachedTypes = new Dictionary<string, Type>();
        }
        #endregion

        #region Methods
        /// <summary>
        /// Creates DTO property
        /// </summary>
        /// <param name="typeBuilder">Type builder</param>
        /// <param name="propertyName">Property name</param>
        /// <param name="supportSuffix">Support property suffix</param>
        /// <param name="propertyType">Property type</param>
        private static void CreateSupportProperty(TypeBuilder typeBuilder, string propertyName, string supportSuffix, Type propertyType)
        {
            // Generate DTO "property"
            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName + supportSuffix, PropertyAttributes.None, propertyType, new Type[0]);

            // Define "get"
            MethodBuilder getMethodBuilder = typeBuilder.DefineMethod("get_" + propertyName + supportSuffix,
                    MethodAttributes.Public |
                    MethodAttributes.SpecialName |
                    MethodAttributes.HideBySig,
                    propertyType, Type.EmptyTypes);

            // Get MethodInfo for base class (DataRow)
            MethodInfo getMethodInfo = typeof(BindableObject).GetMethod("Get" + supportSuffix, new Type[] { typeof(string) });

            // Generate content
            ILGenerator getILGenerator = getMethodBuilder.GetILGenerator();
            getILGenerator.Emit(OpCodes.Ldarg_0); // Load this on stack
            if (string.IsNullOrEmpty(propertyName))
            {
                getILGenerator.Emit(OpCodes.Ldstr, supportSuffix); // Load property name
            }
            else
            {
                getILGenerator.Emit(OpCodes.Ldstr, propertyName); // Load property name
            }
            getILGenerator.Emit(OpCodes.Call, getMethodInfo); // Call GetValue(string) method
            getILGenerator.Emit(OpCodes.Ret); // Return

            // Assign "get" to "property"
            propertyBuilder.SetGetMethod(getMethodBuilder);
        }

        /// <summary>
        /// Creates DTO property
        /// </summary>
        /// <param name="typeBuilder">Type builder</param>
        /// <param name="propertyName">Property name</param>
        private static void CreateProperty(TypeBuilder typeBuilder, string propertyName)
        {
            // Generate DTO "property"
            Type propertyType = Type.GetType("System.String");
            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, new Type[0]);

            // Define "get"
            MethodBuilder getMethodBuilder = typeBuilder.DefineMethod("get_" + propertyName,
                    MethodAttributes.Public |
                    MethodAttributes.SpecialName |
                    MethodAttributes.HideBySig,
                    propertyType, Type.EmptyTypes);

            // Get MethodInfo for base class (DataRow)
            MethodInfo getMethodInfo = typeof(BindableObject).GetMethod("GetValue", new Type[] { typeof(string) });

            // Generate content
            ILGenerator getILGenerator = getMethodBuilder.GetILGenerator();
            getILGenerator.Emit(OpCodes.Ldarg_0); // Load BindableObject on stack
            getILGenerator.Emit(OpCodes.Ldstr, propertyName); // Load property name
            getILGenerator.Emit(OpCodes.Call, getMethodInfo); // Call GetValue(string) method
            getILGenerator.Emit(OpCodes.Ret); // Return

            // Define set
            MethodBuilder setMethodBuilder = typeBuilder.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public |
                  MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig,
                  null, new Type[] { propertyType });

            // Get MethodInfo for base class (DataRow)
            MethodInfo setMethodInfo = typeof(BindableObject).GetMethod("SetValue", new Type[] { typeof(string), propertyType });

            // Generate content
            ILGenerator setILGenerator = setMethodBuilder.GetILGenerator();
            setILGenerator.Emit(OpCodes.Ldarg_0); // Load BindableObject on stack
            setILGenerator.Emit(OpCodes.Ldstr, propertyName); // Load property name
            setILGenerator.Emit(OpCodes.Ldarg_1); // Load value on stack
            setILGenerator.Emit(OpCodes.Call, setMethodInfo); // Call set_Item(string, string) method
            setILGenerator.Emit(OpCodes.Ret);

            // Assign "get" and "set" to "property"
            propertyBuilder.SetGetMethod(getMethodBuilder);
            propertyBuilder.SetSetMethod(setMethodBuilder);
        }

        /// <summary>
        /// Creates DTO type (class)
        /// </summary>
        /// <param name="dataRow">DataRow as a data source</param>
        /// <returns>Newly created type</returns>
        private static Type CreateType(DataRow dataRow)
        {
            // Get type builder from newly created assembly
            // Define dynamic assembly
            AssemblyName assemblyName = new AssemblyName();
            string dtoTypeFullName = dataRow.TypeId;
            assemblyName.Name = dtoTypeFullName + ".dll";
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);

            // Define type
            TypeBuilder typeBuilder = moduleBuilder.DefineType(dtoTypeFullName
                                , TypeAttributes.Public |
                                TypeAttributes.Class |
                                TypeAttributes.AutoClass |
                                TypeAttributes.AnsiClass |
                                TypeAttributes.BeforeFieldInit |
                                TypeAttributes.AutoLayout
                                , typeof(BindableObject));

            // Get ConstructorInfo for base class (BindableObject)
            ConstructorInfo constructorInfo = typeof(BindableObject).GetConstructor(new Type[] { typeof(DataRow) });

            // Define DTO constructor
            Type[] constructorArguments = new Type[] { typeof(DataRow) };
            ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArguments);

            // Generate DTO constructor
            ILGenerator getILGenerator = constructorBuilder.GetILGenerator();
            getILGenerator.Emit(OpCodes.Ldarg_0); // Load BindableObject on stack
            getILGenerator.Emit(OpCodes.Ldarg_1); // Load DataRow on stack
            getILGenerator.Emit(OpCodes.Call, constructorInfo); // Call BindableObject constructor
            getILGenerator.Emit(OpCodes.Ret);

            for (int i = 0; i < dataRow.ItemArray.Count; i++)
            {
                Connector.CreateProperty(typeBuilder, dataRow.ItemArray[i].ColumnName);
                Connector.CreateSupportProperty(typeBuilder, dataRow.ItemArray[i].ColumnName, "HasError", Type.GetType("System.Boolean"));
                Connector.CreateSupportProperty(typeBuilder, dataRow.ItemArray[i].ColumnName, "Error", Type.GetType("System.String"));
            }

            Connector.CreateSupportProperty(typeBuilder, null, "HasNoErrors", Type.GetType("System.Boolean"));
            return typeBuilder.CreateType();
        }

        /// <summary>
        /// Generates bindable object
        /// </summary>
        /// <param name="dataRow">DataRow instance</param>
        /// <returns>Generated bindable object</returns>
        internal BindableObject GetBindableData(DataRow dataRow)
        {
            string dtoTypeFullName = dataRow.TypeId;
            Type dtoType = null;
            if (this._cachedTypes.ContainsKey(dtoTypeFullName))
            {
                // Get type from cache
                dtoType = this._cachedTypes[dtoTypeFullName];
            }
            else
            {
                // Cache new type
                dtoType = Connector.CreateType(dataRow);
                this._cachedTypes.Add(dtoTypeFullName, dtoType);
            }

            return (BindableObject)Activator.CreateInstance(dtoType, new object[] { dataRow });
        }
        #endregion
    }
}
